#!/usr/bin/env bash
## Compresses a video to reduce it's file size.
## by Adnan Shameem; MIT (Expat) license
##
## Usage:
##   Run /path/to/compress-video.sh --help

[ -z "$(command -v ffmpeg)" ] && echo 'ffmpeg not found, please install and continue' && exit 11

## Default config values

# Video dimensions. Check ref below for other 16:9 and 4:3 dimensions
# ref: https://studio.support.brightcove.com/general/optimal-video-dimensions.html
max_width=960
max_height=540
# MKV supports good range of audio, especially vorbis.
# ref: https://trac.ffmpeg.org/wiki/Encode/HighQualityAudio#Containerformats
# If extension changed, you may have trouble with subtitles. Only mkv, mp4 and
# mov support embedding subtitles. Although setting "burn_subtitles" to 1 may
# be an option.
# Single quotes are intentional so that later it can be replaced with actual
# value.
dest_filename='compressed_$source_filename_wo_ext.mkv'
# Single quotes are intentional so that later it can be replaced with actual
# value.
dest_path='$source_path'
# Burn subtitles into the video. 0 disables, 1 enables. Default: 0.
# Shaves some kilobytes off if enabled, but removes the ability to turn
# subtitles off, change language or ever to remove it again from video frames.
# Disabled by default as a safety measure.
burn_subtitles=0
# Keep original audio and do not compress
keep_audio=0

## Compresses given video file
function _process_file() (

	## Internal variables - no need to change these

	source="$1"
	source_path="$(dirname "$1")"
	source_filename="$(basename "$1")"
	source_filename_wo_ext="${source_filename%.*}"

	dest_filename_parsed="${dest_filename//\$source_filename_wo_ext/$source_filename_wo_ext}"
	dest_path_parsed="${dest_path//\$source_path/$source_path}"

	## Check existing file/directory

	[ ! -f "${source_path}/${source_filename}" ] && echo "Input file '${source_path}/${source_filename}' doesn't exist" && exit 14
	[ -d "${dest_path_parsed}/${dest_filename_parsed}" ] && echo "A directory '$(realpath "${dest_path_parsed}/${dest_filename_parsed}")' already exists. Please delete or rename it and try again." && exit 45
	[ ! -d "${dest_path_parsed}" ] && echo "The output directory '$(realpath "${dest_path_parsed}")' does not exist. Creating it..." && mkdir -p "${dest_path_parsed}"
	[ -f "${dest_path_parsed}/${dest_filename_parsed}" ] && echo "'$(realpath "${dest_path_parsed}/${dest_filename_parsed}")' already exists. Press enter to override, or Ctrl+C to cancel..." && read && echo 'Chosen to override...'

	## Notification

	echo "Processing ${source_path}/${source_filename}..."

	## Video filters

	# All array members will be concatenated with "," in between
	video_filters=(
		# "force_divisible_by=2" saves us when the calculated width/height based on
		# aspect ratio is not divisable by 2
		"scale='min(${max_width},iw)':'min(${max_height},ih)':force_original_aspect_ratio=decrease:force_divisible_by=2"
		)

	## Audio arguments

	if [ "$keep_audio" -eq '0' ]; then # "keep-audio" disabled, so compress...
		# ref:
		# - https://trac.ffmpeg.org/wiki/Encode/HighQualityAudio
		# - https://trac.ffmpeg.org/wiki/TheoraVorbisEncodingGuide
		# - https://trac.ffmpeg.org/wiki/Encode/AAC
		audio_args=(-codec:a libvorbis)
	else
		audio_args=(-codec:a copy)
	fi

	# Handle subtitles
	if [ "$burn_subtitles" -ne '0' ]; then # Burn Subtitles
		# Check for .srt subtitle files
		if [ -f "${source_path}/${source_filename_wo_ext}.srt" ]; then
			video_filters+=("subtitles=${source_path}/${source_filename_wo_ext}.srt")
		# Check for .ass subtitle files
		elif [ -f "${source_path}/${source_filename_wo_ext}.ass" ]; then
			video_filters+=("ass=${source_path}/${source_filename_wo_ext}.ass")
		# Take any subtitle data from source file itself
		else
			video_filters+=("subtitles='${source}'")
		fi
	else # Embed subtitles
		# Check for .srt subtitle files
		if [ -f "${source_path}/${source_filename_wo_ext}.srt" ]; then
			subtitle_options=(-f srt -i "${source_path}/${source_filename_wo_ext}.srt")
		# Check for .ass subtitle files
		elif [ -f "${source_path}/${source_filename_wo_ext}.ass" ]; then
			subtitle_options=(-f ass -i "${source_path}/${source_filename_wo_ext}.ass")
		# Take any subtitle data from source file itself
		else
			# Set subtitle codec based on container
			# ref: https://en.m.wikibooks.org/wiki/FFMPEG_An_Intermediate_Guide/subtitle_options#Set_Subtitle_Codec
			ext="${dest_filename_parsed: -4}"
			if [ "$ext" = '.mp4' ] || [ "$ext" = '.mov' ]; then
				subtitle_options+=(-codec:s mov_text)
			elif [ "$ext" = '.mkv' ]; then
				subtitle_options+=(-codec:s srt)
			else
				echo "WARNING: ${ext} format does not support embedding subtitles. Please try setting 'burn_subtitles' to 1 as a remedy."
			fi
		fi
	fi

	## Arguments being passed to ffmpeg.
	# Feel free to comment and change any lines you want!
	ffmpeg_args=(
		# Add arguments to ffmpeg to aid in getting progress
		-progress /dev/stdout
		# Overwrite dest file without asking, because we asked already at the
		# beginning
		-y
		-nostdin
		# Show less output
		-hide_banner -loglevel error -nostats
		# Pass the source file
		-i "$source"
		# Subtitles options determined earlier
		"${subtitle_options[@]}"
		# Set codec as HEVC/H.265 for reduced file size
		# ref: https://spadebee.com/2020/06/06/how-to-highly-compress-videos-using-ffmpeg/
		-codec:v libx265 -crf 28
		# Resize video to fit inside $max_width x $max_height.
		# "force_original_aspect_ratio" is to maintain aspect ratio.
		# Ref: https://trac.ffmpeg.org/wiki/Scaling
		-filter:v "$(IFS=, ; echo "${video_filters[*]}")"
		# Audio arguments
		"${audio_args[@]}"
		# Enable progressive download - for streaming situations
		# Especially for mp4/m4a outputs
		# ref: https://trac.ffmpeg.org/wiki/Encode/AAC#ProgressiveDownload
		-movflags +faststart
		)


	# Get total frames in the source video
	# Ref: https://stackoverflow.com/a/28376817
	total_frames=$(ffprobe -v error -select_streams v:0 -count_packets -show_entries stream=nb_read_packets -of csv=p=0 "$source")

	# Add the output at the end
	ffmpeg_args+=("${dest_path_parsed}/${dest_filename_parsed}")

	## Reset bash time counter to zero.
	# $SECONDS is incremented every second by bash. If this is set to zero, it
	# starts counting from 0 onwards. e.g. 1, 2, 3... This can be used as a time
	# counting measure.
	SECONDS=0

	## Progress bar related vars

	current_frame=0
	encode_fps=0
	prev_progress_print_buffer=''

	## Draw progressbar
	function update_progress() {
		# This is to prevent blinking cursor all over the line when progress is
		# being updated. Instead of printing on screen directly, text is added to
		# this and printed with one single echo -n call.
		print_buffer=''
		# Calculations
		# A clever little trick to calculate float numbers with bash
		# ref: https://stackoverflow.com/a/22406193
		( [ "$current_frame" != '0' ] && [ "$total_frames" != '0' ] ) && progress_percentage=$(awk "BEGIN {printf \"%.2f\",${current_frame}/${total_frames}*100}") || return
		# Progress width in integer. Divide by 3 to make it smaller.
		[ "$progress_percentage" != '0' ] && progress_done_chars=$(( ${progress_percentage%.*} / 3 )) || return
		frames_left=$(( $total_frames - $current_frame ))
		( [ "$frames_left" != '0' ] && [ "${encode_fps%.*}" != '0' ] ) && seconds_left=$(( ( $frames_left / ${encode_fps%.*} ) )) || return
		[ "$seconds_left" != '0' ] && time_left=$(printf '%02d:%02d:%02d' $((seconds_left/3600)) $((seconds_left%3600/60)) $((seconds_left%60))) || return
		# Progress bar
		print_buffer+='['
		for ((i = 0 ; i <= $progress_done_chars; i++)); do print_buffer+='#'; done
	    for ((j = i ; j <= 33 ; j++)); do print_buffer+='-'; done # 100 / 3 = 33
		print_buffer+='] '
	    # Progress text
	    [ -z "$COLUMNS" ] && TERM_COLUMNS="$(tput cols)" || TERM_COLUMNS="$COLUMNS"
	    if [ "$TERM_COLUMNS" -gt '110' ]; then
			print_buffer+="frames: ${current_frame}/${total_frames} (${progress_percentage}%) left: ${frames_left}, ${time_left} - ${encode_fps}fps"
		else
			print_buffer+="${current_frame}/${total_frames}fr (${progress_percentage}%) - ${time_left}"
		fi
		# If same data, do not bother printing it.
		if [ "$prev_progress_print_buffer" != "$print_buffer" ]; then
			echo -n "${print_buffer}" $'\r'
			prev_progress_print_buffer="${print_buffer}"
		fi
	}

	echo -e 'Beginning the compression process, press Ctrl+C anytime to cancel...\n'

	## Run ffmpeg

	ffmpeg "${ffmpeg_args[@]}" | while IFS='=' read -r key value; do
		# There is no guarantee how many lines will come through and which order.
		# But if it is the one we want, we update vars and draw progressbar.
		[ "$key" = 'frame' ] && current_frame="$value" && update_progress
		[ "$key" = 'fps' ]   && encode_fps="$value"    && update_progress
	done

	## If inturrupted (e.g. pressed Ctrl+C)
	if [ 0 -ne "${PIPESTATUS[0]}" ]; then
		echo 'Process inturrupted. Exiting...'
		exit 1012
	fi

	## Done message
	echo 'Done.'
	echo "Time taken: $(($SECONDS / 3600))hrs $((($SECONDS / 60) % 60))min $(($SECONDS % 60))sec"

)

## Prints the help text (e.g. when --help is passed)
function _show_help_text() {
echo "Usage: compress-video.sh [OPTION]... [FILE]...
Compress video FILE(s) to reduce filesize.

  -b, --burn-subtitles     burn subtitles into video
                           WARNING: Cannot be undone!
  -d, --dest-dir DIR       set output directory to DIR
  -h, --max-height HEIGHT  set maximum height of output to HEIGHT
  --help                   show this help and exit
  -k, --keep-audio         keep audio as is, without compressing
  -w, --max-width WIDTH    set maximum width of output to WIDTH

Examples:
  compress-video.sh somevideo.mp4  Compress video and output as
                                   compressed_somevideo.mkv
  compress-video.sh -w 200 -h 200 somevideo.mp4  Compress video as usual and
                                   keep the resolution to 200x200 px
  compress-video.sh *.mp4          Compress all matching .mp4 files

Script location:
<https://notabug.org/adnan360/code-backups/src/master/bash/compress-video.sh>"
}

## Process command line arguments

while [[ "$#" -gt 0 ]]; do
    case $1 in
        -w|--max-width) max_width="$2"; shift ;;
        -h|--max-height) max_height="$2"; shift ;;
        -b|--burn-subtitles) burn_subtitles=1 ;;
        -k|--keep-audio) keep_audio=1 ;;
        -d|--dest-dir) dest_path="$2"; shift ;;
        --help) _show_help_text ;;
        # The for loop is to interpret patterns like *.mp4.
        # [ "$?" -eq 244 ] is to stop when Ctrl+C is pressed and previous file
        # was skipped.
        *) for i in "$1"; do _process_file "$i"; [ "$?" -eq 244 ] && exit 2446; done ;;
    esac
    shift
done
